using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Management.Automation;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;

namespace Microsoft.PowerShell.CmdletManagement.CompareCmdlet
{
    public class CmdletInfo : PsSpecCmdlet
    {
        protected NonameParameterSet _nonameParameterSet = null;
        protected List<NamedParameterSet> _namedParameterSets = new List<NamedParameterSet>();
        protected List<IMismatch> _mismatches = new List<IMismatch>();

        public CmdletInfo(string cmdletName) {
            string[] verbNoun = cmdletName.Split('-');
            this.Verb = verbNoun[0];
            if (verbNoun.Length == 1)
                // A noun does not exist
                this.Noun = "";
            if (verbNoun.Length >= 2) {
                // Restore an original noun
                bool first = true;
                for(int i = 1; i<verbNoun.Length; i++) {
                    if(!first)
                        this.Noun+='-';
                    else
                        first = false;
                    this.Noun += verbNoun[i];
                }
            }
        }

        public CmdletInfo(string verb, string noun) {
            this.Verb = verb;
            this.Noun = noun;
        }

        public NonameParameterSet NonameParameterSet {
            get {return _nonameParameterSet;}
        }

        public List<NamedParameterSet> NamedParameterSets {
            get {return _namedParameterSets;}
        }

        public string CompositeName() {
            return this.Verb + '-' + this.Noun;
        }

		///<summary>Verify if a provided cmdlet has the same name as this cmdlet</summary>
		public bool HasName(CmdletInfo cmdlet) {
			return this.Noun.Equals(cmdlet.Noun, StringComparison.OrdinalIgnoreCase) && 
                this.Verb.Equals(cmdlet.Verb, StringComparison.OrdinalIgnoreCase);
		}

        public void AddNonameParameterSet(NonameParameterSet nonameParameterSet) {
            _nonameParameterSet = nonameParameterSet;
            _nonameParameterSet.CmdletInfo = this;
        }

        public void AddNamedParameterSet(NamedParameterSet namedParameterSet) {
            _namedParameterSets.Add(namedParameterSet);
            namedParameterSet.CmdletInfo = this;
        }

        public int ParameterSetCount {
            get {
                if(_nonameParameterSet != null)
                    return 1;
                return _namedParameterSets.Count;
            }
        }

        protected string ComposeParameterSetNames() {
            bool first = true;
            StringBuilder names = new StringBuilder();
            foreach(NamedParameterSet parameterSet in _namedParameterSets) {
                if(first)
                    first = false;
                else
                    names.Append(", ");
                names.Append(parameterSet.Name);
            }
            return names.ToString();
        }

        public void AddMismatches(CmdletInfo cmdletData) {
            _mismatches.AddRange(cmdletData._mismatches);
        }

        public void AddMismatch(IMismatch mismatch) {
            _mismatches.Add(mismatch);
        }
    }

    public class SpecifiedCmdletInfo : CmdletInfo
    {
        public SpecifiedCmdletInfo(CmdletInfo cmdlet) : base(cmdlet.Verb, cmdlet.Noun) {}
        
        public SpecifiedCmdletInfo(PsSpecCmdletService.PsSpecCmdlet aspenCmdlet) : base(aspenCmdlet.Verb, aspenCmdlet.Noun) {
            this.Snapin = aspenCmdlet.Snapin;
            this.ProjectName = aspenCmdlet.ProjectName;
            this.ShortDescription = aspenCmdlet.ShortDescription;
            this.LongDescription = aspenCmdlet.LongDescription;
            this.SupportsShouldProcess = aspenCmdlet.SupportsShouldProcess;
            this.Notes = aspenCmdlet.Notes;
        }

        public void CompareToImplementation(ImplementedCmdletInfo implementedCmdletInfo) {
            IMismatch mismatch;
            if(this.ParameterSetCount != implementedCmdletInfo.ParameterSetCount) {
                mismatch = new CmdletMismatch(this,
                    String.Format("Specified [{0}] parameter sets but implemented [{1}] parameter sets.",
                                        this.ParameterSetCount, implementedCmdletInfo.ParameterSetCount));
                _mismatches.Add(mismatch);
            }
            if(this._nonameParameterSet != null) {
                // This cmdlet has only no name parameter set
                if(implementedCmdletInfo.NonameParameterSet == null) {
                    // Specified a no name parameter set is not implemented
                    mismatch = new CmdletMismatch(this,
                        String.Format("Specified a no name parameter set but implemented named parameter sets [{0}].", ComposeParameterSetNames()));
                    _mismatches.Add(mismatch);
                    return;
                }
                _nonameParameterSet.CompareToImplementation(implementedCmdletInfo.NonameParameterSet);
            } else {
                // This cmdlet has several named parameter sets
                if(implementedCmdletInfo.NonameParameterSet != null) {
                    //
                    mismatch = new CmdletMismatch(this,
                        String.Format("Specified named parameter sets [{0}] but implemented a no name parameter set.", ComposeParameterSetNames()));
                    _mismatches.Add(mismatch);
                    return;
                }
                // Verify named parameter sets
                foreach(NamedParameterSet specsParameterSet in _namedParameterSets) {
                    bool parameterSetFound = false;
                    foreach(NamedParameterSet implementedParameterSet in implementedCmdletInfo.NamedParameterSets) {
                        if(implementedParameterSet.HasName(specsParameterSet)) {
                            specsParameterSet.CompareToImplementation(implementedParameterSet);
                            parameterSetFound = true;
                            break;
                        }
                    }
                    if(!parameterSetFound) {
                        mismatch = new CmdletMismatch(this,
                            String.Format("The specified parameter set [{0}] is not implemented.", specsParameterSet.Name));
                        _mismatches.Add(mismatch);
                    }
                }
            }
        }

        public static LinkedList<SpecifiedCmdletInfo> RetrieveCmdletInfoFromDatabase(string projectName, string serviceUri) {
            return RetrieveCmdletInfoFromDatabase(projectName, serviceUri, "*");
        }

        public static LinkedList<SpecifiedCmdletInfo> RetrieveCmdletInfoFromDatabase(string projectName, string serviceUri, string cmdletName) {
            LinkedList<SpecifiedCmdletInfo> cmdlets = new LinkedList<SpecifiedCmdletInfo>();

            // Initiate the web service
            PsSpecCmdletService.CmdletService service = new PsSpecCmdletService.CmdletService();
            service.Url = serviceUri;
            service.UseDefaultCredentials = true;

            // Get all cmdlets for this request as a list
            PsSpecCmdletService.PsSpecCmdlet[] aspenCmdlets = service.GetCmdlet(
                PsSpecCmdletUtils.ConvertWildcardToSqlWildcard(cmdletName),
                PsSpecCmdletUtils.ConvertWildcardToSqlWildcard(projectName));
            for(int i = 0; i < aspenCmdlets.Length; i++) {
                cmdlets.AddLast(new SpecifiedCmdletInfo(aspenCmdlets[i]));
            }

            PsSpecCmdletService.PsSpecCmdletParameter[] parametersFromDB;
            List<PsSpecCmdletParameterData> parameters;
            // Get all parameters info for every cmdlet on the list
            foreach(SpecifiedCmdletInfo cmdlet in cmdlets) {

                NonameParameterSet nonameParameterSet = new NonameParameterSet();
                List<NamedParameterSet> namedParameterSets = new List<NamedParameterSet>();

                parametersFromDB = service.GetPsSpecCmdletParameter("%", "%", "%", cmdlet.CompositeName(), cmdlet.ProjectName);
                parameters = new List<PsSpecCmdletParameterData>();

                PsSpecCmdletService.PsSpecCmdletParameterSetEntry[] entries = service.GetPsSpecCmdletParameterSetEntry(cmdlet.ProjectName, cmdlet.CompositeName(), "%", "%");
                foreach(PsSpecCmdletService.PsSpecCmdletParameterSetEntry entry in entries) {
                    // There must be the same number or more of entries than there are parameters
                    // For every entry create a corresponding parameter
                    foreach(PsSpecCmdletService.PsSpecCmdletParameter aspenParameter in parametersFromDB) {
                        if(aspenParameter.Name.Equals(entry.ParameterName, StringComparison.InvariantCultureIgnoreCase)) {
                            // The matching parameter is found
                            PsSpecCmdletParameterData parameter = new PsSpecCmdletParameterData(aspenParameter, entry);
                            parameters.Add(parameter);
                            bool found = false;
                            if(entry.ParameterSetName != null && entry.ParameterSetName != "") {
                                // If a parameter set does not exist, add it
                                foreach(NamedParameterSet parameterSet in namedParameterSets) {
                                    if(parameterSet.HasName(entry.ParameterSetName)) {
                                        // A parameter set already exists
                                        found = true;
                                        parameterSet.AddParameter(parameter);
                                    }
                                }
                                if(!found) {
                                    // A parameter set does not exist. Create a new one
                                    NamedParameterSet nps = new NamedParameterSet(entry.ParameterSetName);
                                    namedParameterSets.Add(nps);
                                    nps.AddParameter(parameter);
                                }
                            } else {
                                // Parameter without a parameter set
                                nonameParameterSet.AddParameter(parameter);
                            }
                        }
                    }
                }
                if(namedParameterSets.Count > 0) {
                    // There are several named parameter sets
                    // Add common parameters to every named parameter set
                    foreach(NamedParameterSet parameterSet in namedParameterSets) {
                        foreach(PsSpecCmdletParameterData parameter in nonameParameterSet.Parameters)
                            parameterSet.AddParameter(parameter);
                        cmdlet.AddNamedParameterSet(parameterSet);
                    }
                } else {
                    // Only a noname parameter set exists, so add it with all parameters
                    cmdlet.AddNonameParameterSet(nonameParameterSet);
                }
            }

            return cmdlets;
        }

        public void AddMismatchesToList(List<IMismatch> store) {
            store.AddRange(_mismatches);
            if(_nonameParameterSet != null) {
                // A single no name parameter set
                _nonameParameterSet.AddMismatchesToList(store);
                return;
            }
            // Multiple named parameter sets
            foreach(NamedParameterSet parameterSet in _namedParameterSets)
                parameterSet.AddMismatchesToList(store);
        }
    }

    public class ImplementedCmdletInfo : CmdletInfo
    {
        private bool _specified = false;
        protected Type _type;

        public ImplementedCmdletInfo(CmdletAttribute cmdletDeclaration, Type type)
                : base(cmdletDeclaration.VerbName, cmdletDeclaration.NounName){
            _type = type;
        }

        public bool Specified {
            get {return _specified;}
            set {_specified = value;}
        }

        public void RestoreImplementedData() {
            // Initially there are no parameter sets
            NonameParameterSet currentParameterSet = null;
            PsSpecCmdletParameterData currentParameter = null;
            //Walk through each public member of cmdlet implementation

            foreach (PropertyInfo classMember in _type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
            {
                //Get attributes of each member and check if they are of interest
                foreach (ParameterAttribute parameter in classMember.GetCustomAttributes(typeof(ParameterAttribute), true))
                {
                    // Every parameter has attribute info
                    string parameterSetName = parameter.ParameterSetName;
                    if(parameterSetName == "__AllParameterSets") {
                        // It is a noname parameter set
                        if(_nonameParameterSet == null)
                            AddNonameParameterSet(new NonameParameterSet());
                        currentParameterSet = _nonameParameterSet;
                    } else {
                        currentParameterSet = RestoreParameterSet(parameterSetName);             
                    }
                    currentParameter = new PsSpecCmdletParameterData();
                    currentParameter.Name = classMember.Name;
                    currentParameter.Type = classMember.PropertyType.Name + ',' + classMember.PropertyType.FullName;
                    // Parameter attribute info
                    currentParameter.Position = parameter.Position;
                    currentParameter.Mandatory = parameter.Mandatory;
                    currentParameter.ValueFromPipeline = parameter.ValueFromPipeline;
                    currentParameter.ValueFromPipelineByPropertyName = parameter.ValueFromPipelineByPropertyName;
                    currentParameter.MemberInfo = classMember;

                    //
                    // In some cmdlets implemented by inheriting from other cmdlets, reflection will return two propertyInfos with the same name.
                    // One cmdlet where this happens is enter-pssession.  What happens is that the base class implements a parameter
                    // with a property and the derived class declares another property with the same name, using the "new" keyword.
                    // Thus, you get two properties with the same name.  Furthermore, in some cmdlets, such as enter-pssession, both
                    // properties have parameter attributes.  
                    //
                    // For these cases we want to find the property of the most derived class and use that one.  That's what we will do
                    // here.  We will check if the parameter set has a parameter with the same name.  If it does, then we will compare
                    // that one with the one we are processing now and we'll keep the one of the most derived type.
                    //
                    // If there, isn't a parameter with a conflicting name, then we can just add the new one to the set.
                    //
                    bool keepExistingParameter = false;
                    PsSpecCmdletParameterData existingParameter;
                    if (currentParameterSet.TryGetParameter(currentParameter.Name, out existingParameter))
                    {
                        if (currentParameter.MemberInfo.DeclaringType.IsSubclassOf(existingParameter.MemberInfo.DeclaringType))
                        {
                            currentParameterSet.RemoveParameter(existingParameter);
                        }
                        else
                        {
                            keepExistingParameter = true;
                        }
                    }

                    if (! keepExistingParameter)
                    {
                        currentParameterSet.AddParameter(currentParameter);
                    }
                }
            }
            if(_namedParameterSets.Count > 0) {
                if(_nonameParameterSet != null) {
                    // Add common parameters to every named parameter set
                    foreach(NamedParameterSet namedParameterSet in _namedParameterSets) {
                        foreach(PsSpecCmdletParameterData parameter in _nonameParameterSet.Parameters) {
                            namedParameterSet.AddParameter(parameter);
                        }
                    }
                }
                _nonameParameterSet = null;
            } else if(_nonameParameterSet == null)
                // This cmdlet does not have any parameters, so add an empty no name parameter set
                AddNonameParameterSet(new NonameParameterSet());
        }

        private NonameParameterSet RestoreParameterSet(string parameterSetName) {
            foreach(NamedParameterSet parameterSet in _namedParameterSets) {
                if(parameterSet.Name == parameterSetName)
                    // A parameter set already exists
                    return parameterSet;
            }
            // A parameter set does not exist. Create a new one
            NamedParameterSet newParameterSet = new NamedParameterSet(parameterSetName);
            AddNamedParameterSet(newParameterSet);
            return newParameterSet;
        }

        public static LinkedList<ImplementedCmdletInfo> RetrieveImplementedCmdlets(List<Assembly> assemblies) {
            LinkedList<ImplementedCmdletInfo> cmdlets = new LinkedList<ImplementedCmdletInfo>();
            ImplementedCmdletInfo cmdlet;
            foreach(Assembly assmbly in assemblies) {
                foreach (Type assemblyType in assmbly.GetTypes()) {
                    foreach (CmdletAttribute cmdletDeclaration in assemblyType.GetCustomAttributes(typeof(CmdletAttribute), true))
                    {
                        if (cmdletDeclaration != null) {
                            // Create a new instance for every implemented cmdlet
                            cmdlet = new ImplementedCmdletInfo(cmdletDeclaration, assemblyType);
                            cmdlets.AddLast(cmdlet);
                            cmdlet.RestoreImplementedData();
                        }
                    }
                }
            }
            return cmdlets;
        }

        public static LinkedList<ImplementedCmdletInfo> RetrieveImplementedCmdlets(List<Assembly> assemblies, Regex name) {
            LinkedList<ImplementedCmdletInfo> cmdlets = new LinkedList<ImplementedCmdletInfo>();
            ImplementedCmdletInfo cmdlet;
            foreach(Assembly assmbly in assemblies) {
                foreach (Type assemblyType in assmbly.GetTypes()) {
                    foreach (CmdletAttribute cmdletDeclaration in assemblyType.GetCustomAttributes(typeof(CmdletAttribute), true))
                    {
                        if (cmdletDeclaration != null &&
                                (name.Match(cmdletDeclaration.VerbName+"-"+cmdletDeclaration.NounName)).Success) {
                            // Create a new instance for a matched implemented cmdlet
                            cmdlet = new ImplementedCmdletInfo(cmdletDeclaration, assemblyType);
                            cmdlets.AddLast(cmdlet);
                            cmdlet.RestoreImplementedData();
                        }
                    }
                }
            }
            return cmdlets;
        }

        public static LinkedList<ImplementedCmdletInfo> RetrieveImplementedCmdlets(List<Assembly> assemblies, string verb, string noun) {
            LinkedList<ImplementedCmdletInfo> cmdlets = new LinkedList<ImplementedCmdletInfo>();
            ImplementedCmdletInfo cmdlet;
            foreach(Assembly assmbly in assemblies) {
                foreach (Type assemblyType in assmbly.GetTypes()) {
                    foreach (CmdletAttribute cmdletDeclaration in assemblyType.GetCustomAttributes(typeof(CmdletAttribute), true))
                    {
                        if (cmdletDeclaration != null &&
                                cmdletDeclaration.VerbName.Equals(verb, StringComparison.CurrentCultureIgnoreCase) &&
                                cmdletDeclaration.NounName.Equals(noun, StringComparison.CurrentCultureIgnoreCase)) {
                            // Create a new instance for a matched implemented cmdlet
                            cmdlet = new ImplementedCmdletInfo(cmdletDeclaration, assemblyType);
                            cmdlets.AddLast(cmdlet);
                            cmdlet.RestoreImplementedData();
                        }
                    }
                }
            }
            return cmdlets;
        }
    }
}
